%include "pm.inc"

;org 010000h ; 不需要org这一句，因为从boot.asm已经使用段间跳转，把CS调整到0x1000处了
jmp LABEL_BEGIN

[SECTION .gdt]
; GDT描述符
; 每一项的格式: 段基址， 段界限，属性
LABEL_GDT:          Descriptor      0,              0,                  0                       ; 空描述符
LABEL_DESC_DATA16:  Descriptor      0,              0ffffh,             DA_DRW                  ; 可读写16位数据段
LABEL_DESC_CODE32:  Descriptor      0,              SegCode32Len - 1,   DA_C + DA_32            ; 32位非一致代码段
LABEL_DESC_CODE16:  Descriptor      0,              0ffffh,             DA_CC0                  ; 16位非一致代码段，在这里跳回到实模式
LABEL_DESC_VIDEO:   Descriptor      0B8000h,        0ffffh,             DA_DRW                  ; 显存地址段（可读可写数据段）
LABEL_DESC_DATA:    Descriptor      0,              DataLen - 1,        DA_DRW                  ; 数据段
LABEL_DESC_STACK:   Descriptor      0,              TopOfStack,         DA_DRWA + DA_32         ; 32位堆栈段
LABEL_DESC_TEST:    Descriptor      0500000h,       0ffffh,             DA_DRW                  ; 测试段，基地址从5M开始

; GDT结束

GdtLen equ $ - LABEL_GDT    ; GDT长度
GdtPtr dw GdtLen - 1        ; GDT界限
       dd 0                 ; GDT基地址


; GDT段选择符
SelectorCode32      equ     LABEL_DESC_CODE32 - LABEL_GDT
SelectorData16      equ     LABEL_DESC_DATA16 - LABEL_GDT
SelectorCode16      equ     LABEL_DESC_CODE16 - LABEL_GDT
SelectorVideo       equ     LABEL_DESC_VIDEO - LABEL_GDT
SelectorData        equ     LABEL_DESC_DATA - LABEL_GDT
SelectorStack       equ     LABEL_DESC_STACK - LABEL_GDT
SelectorTest        equ     LABEL_DESC_TEST - LABEL_GDT

; End of GDT SECTION


[SECTION .data1]
ALIGN 32
[BITS 32]
LABEL_DATA:
SPValueInRealMode dw 0
; 数据段内容定义
PMMessage:          db      "In Protect Mode now", 0    ; 保护模式下的打印
OffsetPMMessage     equ     PMMessage - $$              ; 打印消息的偏移（相对于数据段起始地址）
TestMessage:        db      "DomiAndDole", 0            ; 测试字符串
OffsetTestMessag    equ     TestMessage - $$            ; 测试字符串的偏移（相对于数据段起始地址）

; 数据段长度
DataLen equ $ - LABEL_DATA
; End of SECTION .data1

[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
    times 512 db 0

TopOfStack equ $ - LABEL_STACK - 1
; End of SECTION .gs


[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0100h ; 初始化各个段，sp设置为该值有啥讲究？

    mov ax, RMMessage               ; 待显示字符串
    mov cx, RMMessageLen            ; 待显示字符串长度
    mov dx, 0300h                   ; 显示起始位置，3行0列
    call DispStr

    ; 初始化32位代码段描述符
    xor eax, eax
    mov ax, cs
    shl eax, 4
    add eax, LABEL_SEG_CODE32 ; 实模式下，线性地址=段基址*16+偏移，此处是CS段，偏移就是标识LABEL_SEG_CODE32所在的位置
    mov word [LABEL_DESC_CODE32 + 2], ax ; 将eax寄存器（也就是线性地址）的低16位写入到代码段描述符的第2 3字节（以第0字节开始）
    shr eax, 16 ;  获取线性地址的高16位，低16位在上面的步骤已经使用
    mov byte [LABEL_DESC_CODE32 + 4], al ; 将线性地址的高16位中的低8位写入到段描述符的第4字节
    mov byte [LABEL_DESC_CODE32 + 7], ah ; 将线性地址的高16位中的高8位写入到段描述符的第7字节

    ; 初始化32位数据段，在实模式下，数据段DS和代码段CS的基地址实际上是一样的
    xor eax, eax
    mov ax, ds
    shl eax, 4
    add eax, LABEL_DATA ; 实模式下，线性地址=段基址*16+偏移，此处是DS段，偏移就是标识LABEL_DATA所在的位置
    mov word [LABEL_DESC_DATA + 2], ax ; 将eax寄存器（也就是线性地址）的低16位写入到代码段描述符的第2 3字节（以第0字节开始）
    shr eax, 16 ;  获取线性地址的高16位，低16位在上面的步骤已经使用
    mov byte [LABEL_DESC_DATA + 4], al ; 将线性地址的高16位中的低8位写入到段描述符的第4字节
    mov byte [LABEL_DESC_DATA + 7], ah ; 将线性地址的高16位中的高8位写入到段描述符的第7字节

    ; 初始化16位代码段，会从该代码段重新返回实模式
    xor eax, eax
    mov ax, cs
    shl eax, 4
    add eax, LABEL_SEG_CODE16 ; 实模式下，线性地址=段基址*16+偏移，此处是CS段，偏移就是标识LABEL_SEG_CODE16所在的位置
    mov word [LABEL_DESC_CODE16 + 2], ax ; 将eax寄存器（也就是线性地址）的低16位写入到代码段描述符的第2 3字节（以第0字节开始）
    shr eax, 16 ;  获取线性地址的高16位，低16位在上面的步骤已经使用
    mov byte [LABEL_DESC_CODE16 + 4], al ; 将线性地址的高16位中的低8位写入到段描述符的第4字节
    mov byte [LABEL_DESC_CODE16 + 7], ah ; 将线性地址的高16位中的高8位写入到段描述符的第7字节


    ; 为加载GDTR做准备
    xor eax, eax
    mov ax, ds
    shl eax, 4
    add eax, LABEL_GDT  ; gdt基地址 ---> eax
    mov dword [GdtPtr + 2], eax ; gdt基地址 ---> [GdtPtr + 2]

    ; 加载GDTR
    lgdt [GdtPtr]

    ; 关中断 保护模式下的中断处理机制和实模式下不一样，先简单关中断处理
    cli

    ; 打开地址线A20，历史原因，打开它才能访问超过1M的地址空间
    in al, 92h
    or al, 00000010b
    out 92h, al

    ; 准备切换到保护模式
    mov eax, cr0
    or eax, 1       ; CR0的最低位是PE，该值为1时表示启动分段保护模式，开启PE=1模式后，必须立马使用跳转命令，例如下面的jmp命令
    mov cr0, eax

    ; 真正进入保护模式
    ; 加载描述符SelectorCode32 ---> cs，跳到Code32Selector:0处
    ; 注意前面加入dword的重要性，保证偏移超过16位之后仍然正常，偏移可以是一个32位结果
    jmp dword SelectorCode32:0


    ; 从保护模式跳转到实模式的入口
    ; 会接着从此处开始执行代码
back_to_real_entry:
    ; 重新刷新各个段的内容
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0100h       ; 待定

    ; 关闭A20地址线
    in al, 92h
    and al, 11111101b
    out 92h, al
 
    ; 开中断
    sti

    mov ax, BackToRealMode               ; 待显示字符串
    mov cx, BackToRealModeLen            ; 待显示字符串长度
    mov dx, 0F00h                        ; 显示起始位置
    call DispStr

    jmp $


RMMessage           db  "SYSTEM: in real mode."
RMMessageLen        equ $ - RMMessage
BackToRealMode      db  "SYSTEM: back to real mode."
BackToRealModeLen   equ $ - BackToRealMode

DispStr:
    mov bp, ax          ; 假设调用前显示字符串的地址已经存放在寄存器ax中
                        ; 假设待显示字符个数存放在cx寄存器中，因此此处不需要设置
    mov ah, 13h         ; 功能码13h
    mov al, 1           ; 显示方式为1：字符串中只包含字符，显示属性在bl中，光标位置改变

    mov bl, 0ch         ; 显示属性：黑底红字
    mov bh, 0           ; 页码 0，当前还没有滚屏操作

                        ; 假设调用前dx已经存放了起始坐标
    int 10h
    ret
; DispStr 结束

; End of [SECTION .s16]


[SECTION .s32] ; 32位代码段，从实模式跳入
[BITS 32]
LABEL_SEG_CODE32:
    mov ax, SelectorData
    mov ds, ax  ; 数据段选择符
    mov ax, SelectorTest
    mov es, ax  ; 测试段选择符
    mov ax, SelectorVideo
    mov gs, ax  ; 视频段选择符
    mov ax, SelectorStack
    mov ss, ax  ; 堆栈段选择符

    ; 下面先显示进入保护模式的字符串打印
    mov ah, 0ch     ; 0000 黑底， 000C 红字
    xor esi, esi
    xor edi, edi
    mov esi, OffsetPMMessage    ; 源数据偏移
    mov edi, (80 * 10 + 0) * 2  ; 屏幕第10行，第0列 
    cld
.disp:
    lodsb
    test al, al
    jz .finished
    mov [gs:edi], ax
    add edi, 2
    jmp .disp
.finished:  ; 显示完成
    call DispReturn
    call TestRead
    call TestWrite
    call TestRead

    ;mov edi, (80 * 11 + 79) * 2     ; 屏幕第11行，第79列
    ;mov ah, 0ch     ; 0000 黑底， 000C 红字
    ;mov al, 'S'
    ;mov [gs:edi], ax

    ; 死循环
    ;jmp $

    ; 从这里跳到16位代码，为跳回到实模式做准备
    jmp SelectorCode16:0

; 从测试段（5M地址）读取16字节
TestRead:
    xor esi, esi
    mov ecx, 16
.loop:
    mov al, [es:esi]
    call DispAL
    inc esi
    loop .loop
    call DispReturn
    ret
; TestRead 结束


; 将数据段的字符串写入到测试段
TestWrite:
    push esi
    push edi
    xor edi, edi
    xor esi, esi
    mov esi, OffsetTestMessag    ; 源数据偏移
    cld
.1:
    lodsb
    test al, al
    jz .2   ; 结束
    mov [es:edi], al ; 写入到测试段
    inc edi  ; 测试段偏移
    jmp .1
.2:
    pop edi
    pop esi
    ret
; TestWrite 结束

; 显示AL寄存器中的数 --- 将数转换成字符后显示
; 例如al = 1010 0002，则显示为对应的字符"A2"
; 默认数字已经存放在AL寄存器中，edi已经指向下一个要显示的位置
; 被改变的寄存器：ax,edi
DispAL:
    push ecx
    push edx

    mov ah, 0ch     ; 0000 黑底， 000C 红字
    mov dl, al      ; 将al内容保存起来
    shr al, 4       ; al右移4位，相当于获取al的高4位，先把高4位转换成字符显示出来
    mov ecx, 2      ; ecx = 2, loop循环2次，因为要将al（字节）的高位和地位分2次显示出来
.begin:
    and al, 01111b  ; 获取al的低4位，第一次循环时候，al的值是函数调用传入al的高4位，第二次循环的时候al的值是函数调用传入的al的低4位
    cmp al, 9       ; 数字与9进行比较
    ja .1           ; >9 则跳转到.1执行
    add al, '0'     ; <=9，转换成对应的字符，例如4 -> ‘4’
    jmp .2          ; 跳转到显示字符的地方
.1:
    sub al, 0ah     ; 对于大于等于10的数字，先减去10
    add al, 'A'     ; 然后转换成对应的十六进制数字，例如 13 = (13 - 10) + 'A' -> 'D'
.2:
    mov [gs:edi], ax    ; ah保存字的性质（黑底红字），al保存转换成字符数字，显示出来
    add edi, 2          ; 显示的位置偏移

    mov al, dl      ; 获取原al的值，实际上是取原al的低4位，因为高4位在第一次循环已经做了显示处理
    loop .begin

    add edi, 2      ; 相当于显示完AL对应的两个字符后，空出一个位置（有空格的效果）

    pop edx
    pop ecx
    ret
; DispAL 结束


; 将显示位置移动到下一行的起始位置
DispReturn:
    push eax
    push ebx

    mov eax, edi
    mov bl, 160     ; 每一行80个字符+80个字符的属性，总计160个字符
    div bl          ; eax=eax/bl，表示当前总共有几行

    and eax, 0FFh
    inc eax         ; 将显示的行数+1

    mov bl, 160
    mul bl          ; 显示的行数 * 每行的字符数量 = 下一个要显示的位置

    mov edi, eax    ; 将下一行显示的位置存储到edi供其他程序使用

    pop ebx
    pop eax
    ret
; DispReturn 结束


SegCode32Len equ $ - LABEL_SEG_CODE32
; End of [SECTION .s32]


; 下面的代码是16位，在保护模式下执行
; 先将一个合适的16位数据描述符通过段选择符加载，刷新DS/SS/ES等寄存器
; 然后，执行相关操作恢复实模式
; 最后，长跳转到实模式代码段下执行
[SECTION .s16Code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
    ; 加载16位数据段内容到对应的段寄存器
    ; 不能直接在保护模式下从32位代码段返回
    ; 要保证跳入实模式之前，各个段已经有合适的属性值
    mov ax, SelectorData16
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov fs, ax
    mov gs, ax

    ; 关闭分段，CR0的PE
    mov eax, cr0
    and al, 11111110b
    mov cr0, eax

    ; 长跳转到实模式的代码段
    ; - 实模式跳转到保护模式时的CS段值为010000h，此处采用的方式
    ; - 此处也可以在实模式阶段，直接将CS值写到此处（也就是back_to_real_mode[3]和back_to_real_mode[4]），格式如下：
    ;   BYTE1 BYTE2-3 BYTE4-5
    ;   jmp   Offset  Segment
    ;   具体需要在实模式代码下执行
    ;   mov ax, cs
    ;   mov [back_to_real_mode + 3], ax
    ; 不能直接从32位代码段跳入实模式，因为跳转指令无法实现在32位代码段下刷新CS的属性值，因此先进入保护模式的16位代码段刷新好属性
back_to_real_mode:
    jmp 01000h:back_to_real_entry

DataLenCode16 equ $ - LABEL_SEG_CODE16

; End of [SECTION .s16Code]